package dkbta.client.domain;

import dkbta.Config;
import dkbta.client.domain.exceptions.ClientConnectionFailureException;
import dkbta.client.domain.exceptions.FailedToOpenFileException;
import dkbta.client.gui.forms.SubmitFilenameForm;
import dkbta.server.domain.cu.CUStub;
import dkbta.server.util.query.Query;
import dkbta.server.util.query.Results;
import static dkbta.util.Utils.xmlToString;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.LinkedBlockingQueue;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;



/**
 * @author Arkady Mishiev
 *
 */

public class ClientKBTA extends Thread implements Observable{

	private String m_state;
	private Observer m_stateObserver;
	private volatile boolean m_shouldRun;
	private String m_serverIp;
	private int m_serverPort;
	private LinkedBlockingQueue<String> m_fileNameQueue;
	private static Logger s_logger;
	private Query m_currQuery;
	private Results m_currResults;
	private Socket m_socket;
	private PrintWriter m_out;
	private BufferedReader m_in;
	private final String WAIT_FOR_RESULTS = "WAIT_FOR_RESULTS";
	private final String WAIT_FOR_QUERY = "WAIT_FOR_QUERY";	
	private String m_leftOver;
	private volatile boolean m_isConnected;

	public ClientKBTA(LinkedBlockingQueue<String> filenameQueue){
		super("Client");
		m_leftOver = "";
		m_shouldRun = true;
		m_fileNameQueue = filenameQueue;
		m_state = WAIT_FOR_QUERY;
		m_serverIp = Config.getString("SERVER_IP","123");
		m_serverPort = Config.getInt("CLIENTS_REGISTRY_PORT", 5000);
		setConnectionState(false);		
	}

	public void run(){
		String line = "Initialized ok. state :" + m_state +	". Disconnected";
		s_logger.info(line);
		m_stateObserver.updateState(line);
		String filename = null;
		while(m_shouldRun){			
			try {
				m_state = WAIT_FOR_QUERY;
				filename = m_fileNameQueue.take();
				if(isConnected()){
					m_currQuery = getQuery(filename);
					sendQuery(m_currQuery);
					m_state = WAIT_FOR_RESULTS;
					m_stateObserver.updateState(m_state);
					s_logger.info("State :" + m_state);
					m_currResults = receiveResults();									
					m_state = WAIT_FOR_QUERY;
					s_logger.info("State :" + m_state);
					m_stateObserver.updateState(m_state);
				}
				else{
					m_stateObserver.updateState("Client is not connected!");
				}
			} catch(FailedToOpenFileException e){
				s_logger.warn("Failed to open file " + filename + ". Try again");
				m_stateObserver.updateState("Failed to open file " + filename + ". Try again");                            
			}
			catch (ClientConnectionFailureException e) {
				s_logger.warn("Connection failed! try connect again");
				m_stateObserver.updateState("Connection failed! try connect again");
				disconnect();
				m_state = WAIT_FOR_QUERY;
				m_stateObserver.updateState(WAIT_FOR_QUERY);
			} catch (InterruptedException e) {
				s_logger.warn("Interrupted while waiting for filename!");
				m_stateObserver.updateState("Failed to get filename. try again");
			}
		}
	}

	private void sendQuery(Query query) throws ClientConnectionFailureException{
		try{
			s_logger.info("Sending query. id :" + query.getId());
			m_stateObserver.updateState("Sending query. id :" + query.getId());
			m_out.print(query.getXMLString() + Config.FRAME);
			m_out.flush();
			s_logger.info("Query sent ok. id :" + query.getId());
			m_stateObserver.updateState("Query sent ok. id :" + query.getId());
		}catch(Exception e){
			throw new ClientConnectionFailureException();
		}
	}


	private Results receiveResults() throws ClientConnectionFailureException {
		String line = "Waiting for results from server ...";
		s_logger.info(line);
		m_stateObserver.updateState(line);		
		String results_str;
		try {
			results_str = nextToken();
			if(results_str.startsWith(Config.FAILURE_PREFIX)){					
				String log = "Failure received. Reason : " + 
				results_str.substring(Config.FAILURE_PREFIX.length());
				s_logger.info(log);
				m_stateObserver.updateState(log);
			}
			else{
				String log = "Results received. - " + results_str;
				s_logger.info(log);
				m_stateObserver.updateState(log);				
			}
			Results res = new Results("-NoNeedQueryId-", results_str,"-NoNeedClientId-");			
			return res;			
		} 
		catch (Exception e) {
			throw new ClientConnectionFailureException();
		}
	}	


	private Query getQuery(String filename) throws FailedToOpenFileException {
		s_logger.info("Loading a Plan file " + filename + " ...");
		m_stateObserver.updateState("Loading a Plan file " + filename + " ...");		
		try {
			DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
			builderFactory.setNamespaceAware(true);
			builderFactory.setIgnoringElementContentWhitespace(false);
			DocumentBuilder builder = builderFactory.newDocumentBuilder();  
			Document xmlDoc = builder.parse(filename);
			//			Transformer transformer = TransformerFactory.newInstance().newTransformer();
			//			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			//			StreamResult result = new StreamResult(new StringWriter());
			//			DOMSource source = new DOMSource(xmlDoc);
			//			transformer.transform(source, result);
			String xmlString = xmlToString(xmlDoc.getDocumentElement());//result.getWriter().toString();		
			Query q = new Query(xmlString,"-NoNeedClientIdHere-");
			s_logger.info("Plan file " + filename + " loaded ok");
			m_stateObserver.updateState("Plan file " + filename + " loaded ok");
			return q;

		} catch (Exception e) {
			//			s_logger.warn("Failed to read query file");
			//			m_stateObserver.updateState("Failed to read query file");
			throw new FailedToOpenFileException();
		}  
	}

	public String getClientsState() {
		return m_state;
	}

	public synchronized boolean isConnected() {
		return m_isConnected;
	}

	private void setConnectionState(boolean b) {
		m_isConnected = b;		
	}

	public String getServerIP() {
		return m_serverIp;
	}

	public int getServerPort() {
		return m_serverPort;
	}

	@Override
	public void addObserver(Observer o) {
		// TODO Auto-generated method stub
		m_stateObserver = o;
	}

	private String nextToken() throws IOException, ClientConnectionFailureException {
		String ans = m_leftOver;
		String toReturn = null;
		boolean readMore = true;
		final char[] charBuffer = new char[10000];
		final StringBuilder sb = new StringBuilder();
		int k ;
		while(readMore){
			k = m_in.read(charBuffer);
			if(k == -1){
				throw new ClientConnectionFailureException();
			}
			sb.append(charBuffer,0,k);
			ans = ans + sb.toString();
			if(ans.contains(Config.FRAME)){
				readMore = false;
				String left = ans.substring(ans.indexOf(Config.FRAME) + 
						Config.FRAME.length(),ans.length());
				m_leftOver=left;
				ans = ans.substring(0,ans.indexOf(Config.FRAME));
				toReturn = ans.replace(Config.FRAME,"");
			}        
		}
		return toReturn;
	}	

	public void connect() {
		if(isConnected()){
			m_stateObserver.updateState("The client is already connected!");
			return;
		}
		String line = "Connecting ...";
		s_logger.info(line);
		m_stateObserver.updateState(line);
		try {					
			m_socket = new Socket(m_serverIp, m_serverPort);
			m_out = new PrintWriter(m_socket.getOutputStream(), true);
			m_in = new BufferedReader(new InputStreamReader(m_socket.getInputStream()));				
			m_state = WAIT_FOR_QUERY;
			m_stateObserver.updateState(m_state);
			s_logger.info("Connected ok");
			setConnectionState(true);
			m_stateObserver.updateState("Connected ok");
		} catch (UnknownHostException e) {
			s_logger.warn("Don't know about host <" + m_serverIp + ">");
			setConnectionState(false);
			m_stateObserver.updateState("Don't know about host <" + m_serverIp + ">");
		} catch (IOException e) {
			setConnectionState(false);
			s_logger.warn("Couldn't get I/O for the connection to " + 
					m_serverIp + " : " + m_serverPort);
			m_stateObserver.updateState("Couldn't get I/O for the connection to " +
					m_serverIp + " : " + m_serverPort);
		} finally{
			if(!isConnected()){
				try{
					m_socket = null;
					m_out = null;
					m_in = null;
				} catch(Exception e){}
			}
		}
	}

	public synchronized void disconnect(){
		s_logger.info("Disconnecting ...");
		m_stateObserver.updateState("Disconnecting ...");
		try{
			if(m_in != null){
				m_in.close();
				m_in = null;
			}
			if(m_out != null){
				m_out.close();
				m_out = null;
			}
			if(m_socket != null){
				m_socket.close();
				m_socket = null;
			}
			setConnectionState(false);
			m_stateObserver.updateState("Disconnected");
			s_logger.info("Disconnected");
		}catch(Exception e){
			s_logger.warn("Failed to disconnect");
			m_stateObserver.updateState("Failed to disconnect");
			//e.printStackTrace();
		}
	}

	public static void main(String[] args){
		PropertyConfigurator.configure("./files/conf/client.log4j.properties");
		s_logger = Logger.getLogger(ClientKBTA.class);
		LinkedBlockingQueue<String> filenameQueue = new LinkedBlockingQueue<String>();		
		ClientKBTA controller = new ClientKBTA(filenameQueue);				
		SubmitFilenameForm submitForm = new SubmitFilenameForm(filenameQueue, controller);
		submitForm.setVisible(true);
		controller.start();
	}




}
